﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="TypesScanner.cs" company="CodePlex">
//   The MIT License
//   Copyright (c) 2013 Julien Blin
//   
//   Permission is hereby granted, free of charge, to any person obtaining a copy
//   of this software and associated documentation files (the "Software"), to deal
//   in the Software without restriction, including without limitation the rights
//   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//   copies of the Software, and to permit persons to whom the Software is
//   furnished to do so, subject to the following conditions:
//   
//   The above copyright notice and this permission notice shall be included in
//   all copies or substantial portions of the Software.
//   
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//   THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace UCodeFirst
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text.RegularExpressions;
    using System.Web.Hosting;

    using Umbraco.Core;
    using Umbraco.Core.Logging;

    /// <summary>
    /// Utility class that helps scanning assemblies in bin directory for types.
    /// Type information is cached after first execution.
    /// </summary>
    public static class TypesScanner
    {
        /// <summary>
        /// Loaded assemblies in bin.
        /// </summary>
        public static readonly Lazy<IEnumerable<Assembly>> AssembliesInBin = new Lazy<IEnumerable<Assembly>>(ScanAssembliesInBin, true);

        /// <summary>
        /// All the types in the <see cref="AssembliesInBin"/>.
        /// </summary>
        public static readonly Lazy<IEnumerable<Type>> TypesInBin = new Lazy<IEnumerable<Type>>(
            () => AssembliesInBin.Value.SelectMany(x => x.GetTypes()).ToList(),
            true);

        /// <summary>
        /// Returns <b>classes</b> in the bin directory that are assignable from <typeparamref name="T"/>
        /// (i.e. either implements or derived from it).
        /// </summary>
        /// <typeparam name="T">
        /// The base type to search for. Can be an interface or a class.
        /// </typeparam>
        /// <param name="excludebaseType">
        /// Indicates whether to filter out the base type or not. Defaults to true.
        /// </param>
        /// <returns>
        /// The classes found.
        /// </returns>
        public static IEnumerable<Type> GetClassesInBinThatAreAssignableFrom<T>(bool excludebaseType = true)
        {
            return GetClassesInBinThatAreAssignableFrom(typeof(T), excludebaseType);
        }

        /// <summary>
        /// Returns <b>classes</b> in the bin directory that are assignable from <paramref name="baseType"/>
        /// (i.e. either implements or derived from it).
        /// </summary>
        /// <param name="baseType">
        /// The base type to search for. Can be an interface or a class.
        /// </param>
        /// <param name="excludebaseType">
        /// Indicates whether to filter out the base type or not. Defaults to true.
        /// </param>
        /// <returns>
        /// The classes found.
        /// </returns>
        public static IEnumerable<Type> GetClassesInBinThatAreAssignableFrom(Type baseType, bool excludebaseType = true)
        {
            return TypesInBin.Value.Where(x => x.IsClass && baseType.IsAssignableFrom(x) && (!excludebaseType || (x != baseType)));
        }

        /// <summary>
        /// Returns <see cref="MatchedResource"/> for embedded resources in assemblies in bin directory
        /// that matches the <paramref name="patternRegex"/>
        /// </summary>
        /// <param name="patternRegex">
        /// The pattern regex to match.
        /// </param>
        /// <returns>
        /// The <see cref="MatchedResource"/>.
        /// </returns>
        public static IEnumerable<MatchedResource> GetResourcesInBin(string patternRegex)
        {
            var compiledRegex = new Regex(patternRegex, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
// ReSharper disable LoopCanBeConvertedToQuery
            foreach (var res in AssembliesInBin.Value.Select(assembly => new { Assembly = assembly, Matches = assembly.GetManifestResourceNames().Select(x => compiledRegex.Match(x)).Where(x => x.Success) }))
// ReSharper restore LoopCanBeConvertedToQuery
            {
                foreach (var match in res.Matches)
                {
// ReSharper disable AssignNullToNotNullAttribute
                    using (var reader = new StreamReader(res.Assembly.GetManifestResourceStream(match.Value)))
// ReSharper restore AssignNullToNotNullAttribute
                    {
                        yield return new MatchedResource { ResourceName = match.Value, Match = match, Content = reader.ReadToEnd(), Assembly = res.Assembly };
                    }
                }
            }
        }

        /// <summary>
        /// Scan the bin directory and load assemblies that can be loaded.
        /// Does not throw exceptions.
        /// </summary>
        /// <returns>
        /// The loaded assemblies.
        /// </returns>
        private static IEnumerable<Assembly> ScanAssembliesInBin()
        {
            var result = new List<Assembly>();
            var binFolder = HostingEnvironment.MapPath("~/bin");
            if (binFolder == null)
            {
                return Enumerable.Empty<Assembly>();
            }

// ReSharper disable AssignNullToNotNullAttribute
            using (var umbracoAssembliesReader = new StreamReader(typeof(TypesScanner).Assembly.GetManifestResourceStream("UCodeFirst.UmbracoAssemblies.txt")))
// ReSharper restore AssignNullToNotNullAttribute
            {
                var umbracoAssemblies = umbracoAssembliesReader.ReadToEnd().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var assemblyFile in Directory.EnumerateFiles(binFolder, "*.dll", SearchOption.TopDirectoryOnly).Where(x => !umbracoAssemblies.Contains(Path.GetFileName(x))))
                {
                    try
                    {
                        result.Add(Assembly.LoadFrom(assemblyFile));
                    }
                    catch (Exception ex)
                    {
                        LogHelper.WarnWithException(typeof(TypesScanner), Resources.WarningWhileLoadingAssemblyInBin.InvariantFormat(assemblyFile), false, ex);
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// Represent an embedded resource match by the <see cref="TypesScanner"/>.
        /// </summary>
        public class MatchedResource
        {
            /// <summary>
            /// Gets or sets the resource name.
            /// </summary>
            public string ResourceName { get; set; }

            /// <summary>
            /// Gets or sets the regular expression match.
            /// </summary>
            public Match Match { get; set; }

            /// <summary>
            /// Gets or sets the content.
            /// </summary>
            public string Content { get; set; }

            /// <summary>
            /// Gets or sets the assembly that contains the resource.
            /// </summary>
            public Assembly Assembly { get; set; }
        }
    }
}
